/**
 *    Copyright ${license.git.copyrightYears} the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.generator.internal;

import static org.mybatis.generator.internal.util.StringUtility.stringHasValue;
import static org.mybatis.generator.internal.util.messages.Messages.getString;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.mybatis.generator.api.CommentGenerator;
import org.mybatis.generator.api.ConnectionFactory;
import org.mybatis.generator.api.FullyQualifiedTable;
import org.mybatis.generator.api.JavaFormatter;
import org.mybatis.generator.api.Plugin;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.JavaTypeResolver;
import org.mybatis.generator.api.XmlFormatter;
import org.mybatis.generator.api.dom.DefaultJavaFormatter;
import org.mybatis.generator.api.dom.DefaultXmlFormatter;
import org.mybatis.generator.codegen.ibatis2.IntrospectedTableIbatis2Java2Impl;
import org.mybatis.generator.codegen.ibatis2.IntrospectedTableIbatis2Java5Impl;
import org.mybatis.generator.codegen.mybatis3.IntrospectedTableMyBatis3Impl;
import org.mybatis.generator.codegen.mybatis3.IntrospectedTableMyBatis3SimpleImpl;
import org.mybatis.generator.config.CommentGeneratorConfiguration;
import org.mybatis.generator.config.ConnectionFactoryConfiguration;
import org.mybatis.generator.config.Context;
import org.mybatis.generator.config.PluginConfiguration;
import org.mybatis.generator.config.JavaTypeResolverConfiguration;
import org.mybatis.generator.config.PropertyRegistry;
import org.mybatis.generator.config.TableConfiguration;
import org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl;

/**
 * This class creates the different objects needed by the generator.
 *
 * @author Jeff Butler
 */
public class ObjectFactory {

  /** The external class loaders. */
  private static List<ClassLoader> externalClassLoaders;

  /** The resource class loaders. */
  private static List<ClassLoader> resourceClassLoaders;

  static {
    externalClassLoaders = new ArrayList<ClassLoader>();
    resourceClassLoaders = new ArrayList<ClassLoader>();
  }

  /**
   * Utility class. No instances allowed
   */
  private ObjectFactory() {
    super();
  }

  /**
   * Clears the class loaders. This method should be called at the beginning of a generation run so that and change to
   * the classloading configuration will be reflected. For example, if the eclipse launcher changes configuration it
   * might not be updated if eclipse hasn't been restarted.
   * 
   */
  public static void reset() {
    externalClassLoaders.clear();
    resourceClassLoaders.clear();
  }

  /**
   * Adds a custom classloader to the collection of classloaders searched for resources. Currently, this is only used
   * when searching for properties files that may be referenced in the configuration file.
   *
   * @param classLoader
   *          the class loader
   */
  public static synchronized void addResourceClassLoader(ClassLoader classLoader) {
    ObjectFactory.resourceClassLoaders.add(classLoader);
  }

  /**
   * Adds a custom classloader to the collection of classloaders searched for "external" classes. These are classes that
   * do not depend on any of the generator's classes or interfaces. Examples are JDBC drivers, root classes, root
   * interfaces, etc.
   *
   * @param classLoader
   *          the class loader
   */
  public static synchronized void addExternalClassLoader(ClassLoader classLoader) {
    ObjectFactory.externalClassLoaders.add(classLoader);
  }

  /**
   * This method returns a class loaded from the context classloader, or the classloader supplied by a client. This is
   * appropriate for JDBC drivers, model root classes, etc. It is not appropriate for any class that extends one of the
   * supplied classes or interfaces.
   *
   * @param type
   *          the type
   * @return the Class loaded from the external classloader
   * @throws ClassNotFoundException
   *           the class not found exception
   */
  public static Class<?> externalClassForName(String type) throws ClassNotFoundException {

    Class<?> clazz;

    for (ClassLoader classLoader : externalClassLoaders) {
      try {
        clazz = Class.forName(type, true, classLoader);
        return clazz;
      } catch (Throwable e) {
        // ignore - fail safe below
      }
    }

    return internalClassForName(type);
  }

  /**
   * Creates a new Object object.
   *
   * @param type
   *          the type
   * @return the object
   */
  public static Object createExternalObject(String type) {
    Object answer;

    try {
      Class<?> clazz = externalClassForName(type);
      answer = clazz.newInstance();
    } catch (Exception e) {
      throw new RuntimeException(getString("RuntimeError.6", type), e); //$NON-NLS-1$
    }

    return answer;
  }

  /**
   * Internal class for name.
   *
   * @param type
   *          the type
   * @return the class
   * @throws ClassNotFoundException
   *           the class not found exception
   */
  public static Class<?> internalClassForName(String type) throws ClassNotFoundException {
    Class<?> clazz = null;

    try {
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      clazz = Class.forName(type, true, cl);
    } catch (Exception e) {
      // ignore - failsafe below
    }

    if (clazz == null) {
      clazz = Class.forName(type, true, ObjectFactory.class.getClassLoader());
    }

    return clazz;
  }

  /**
   * Gets the resource.
   *
   * @param resource
   *          the resource
   * @return the resource
   */
  public static URL getResource(String resource) {
    URL url;

    for (ClassLoader classLoader : resourceClassLoaders) {
      url = classLoader.getResource(resource);
      if (url != null) {
        return url;
      }
    }

    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    url = cl.getResource(resource);

    if (url == null) {
      url = ObjectFactory.class.getClassLoader().getResource(resource);
    }

    return url;
  }

  /**
   * Creates a new Object object.
   *
   * @param type
   *          the type
   * @return the object
   */
  public static Object createInternalObject(String type) {
    Object answer;

    try {
      Class<?> clazz = internalClassForName(type);

      answer = clazz.newInstance();
    } catch (Exception e) {
      throw new RuntimeException(getString("RuntimeError.6", type), e); //$NON-NLS-1$

    }

    return answer;
  }

  /**
   * Creates a new Object object.
   *
   * @param context
   *          the context
   * @param warnings
   *          the warnings
   * @return the java type resolver
   */
  public static JavaTypeResolver createJavaTypeResolver(Context context, List<String> warnings) {
    JavaTypeResolverConfiguration config = context.getJavaTypeResolverConfiguration();
    String type;

    if (config != null && config.getConfigurationType() != null) {
      type = config.getConfigurationType();
      if ("DEFAULT".equalsIgnoreCase(type)) { //$NON-NLS-1$
        type = JavaTypeResolverDefaultImpl.class.getName();
      }
    } else {
      type = JavaTypeResolverDefaultImpl.class.getName();
    }

    JavaTypeResolver answer = (JavaTypeResolver) createInternalObject(type);
    answer.setWarnings(warnings);

    if (config != null) {
      answer.addConfigurationProperties(config.getProperties());
    }

    answer.setContext(context);

    return answer;
  }

  /**
   * Creates a new Object object.
   *
   * @param context
   *          the context
   * @param pluginConfiguration
   *          the plugin configuration
   * @return the plugin
   */
  public static Plugin createPlugin(Context context, PluginConfiguration pluginConfiguration) {
    Plugin plugin = (Plugin) createInternalObject(pluginConfiguration.getConfigurationType());
    plugin.setContext(context);
    plugin.setProperties(pluginConfiguration.getProperties());
    return plugin;
  }

  /**
   * Creates a new Object object.
   *
   * @param context
   *          the context
   * @return the comment generator
   */
  public static CommentGenerator createCommentGenerator(Context context) {

    CommentGeneratorConfiguration config = context.getCommentGeneratorConfiguration();
    CommentGenerator answer;

    String type;
    if (config == null || config.getConfigurationType() == null) {
      type = DefaultCommentGenerator.class.getName();
    } else {
      type = config.getConfigurationType();
    }

    answer = (CommentGenerator) createInternalObject(type);

    if (config != null) {
      answer.addConfigurationProperties(config.getProperties());
    }

    return answer;
  }

  public static ConnectionFactory createConnectionFactory(Context context) {

    ConnectionFactoryConfiguration config = context.getConnectionFactoryConfiguration();
    ConnectionFactory answer;

    String type;
    if (config == null || config.getConfigurationType() == null) {
      type = JDBCConnectionFactory.class.getName();
    } else {
      type = config.getConfigurationType();
    }

    answer = (ConnectionFactory) createInternalObject(type);

    if (config != null) {
      answer.addConfigurationProperties(config.getProperties());
    }

    return answer;
  }

  /**
   * Creates a new Object object.
   *
   * @param context
   *          the context
   * @return the java formatter
   */
  public static JavaFormatter createJavaFormatter(Context context) {
    String type = context.getProperty(PropertyRegistry.CONTEXT_JAVA_FORMATTER);
    if (!stringHasValue(type)) {
      type = DefaultJavaFormatter.class.getName();
    }

    JavaFormatter answer = (JavaFormatter) createInternalObject(type);

    answer.setContext(context);

    return answer;
  }

  /**
   * Creates a new Object object.
   *
   * @param context
   *          the context
   * @return the xml formatter
   */
  public static XmlFormatter createXmlFormatter(Context context) {
    String type = context.getProperty(PropertyRegistry.CONTEXT_XML_FORMATTER);
    if (!stringHasValue(type)) {
      type = DefaultXmlFormatter.class.getName();
    }

    XmlFormatter answer = (XmlFormatter) createInternalObject(type);

    answer.setContext(context);

    return answer;
  }

  /**
   * Creates a new Object object.
   *
   * @param tableConfiguration
   *          the table configuration
   * @param table
   *          the table
   * @param context
   *          the context
   * @return the introspected table
   */
  public static IntrospectedTable createIntrospectedTable(TableConfiguration tableConfiguration,
      FullyQualifiedTable table, Context context) {

    IntrospectedTable answer = createIntrospectedTableForValidation(context);
    answer.setFullyQualifiedTable(table);
    answer.setTableConfiguration(tableConfiguration);

    return answer;
  }

  /**
   * This method creates an introspected table implementation that is only usable for validation (i.e. for a context to
   * determine if the target is ibatis2 or mybatis3).
   * 
   *
   * @param context
   *          the context
   * @return the introspected table
   */
  public static IntrospectedTable createIntrospectedTableForValidation(Context context) {
    String type = context.getTargetRuntime();
    if (!stringHasValue(type)) {
      type = IntrospectedTableMyBatis3Impl.class.getName();
    } else if ("Ibatis2Java2".equalsIgnoreCase(type)) { //$NON-NLS-1$
      type = IntrospectedTableIbatis2Java2Impl.class.getName();
    } else if ("Ibatis2Java5".equalsIgnoreCase(type)) { //$NON-NLS-1$
      type = IntrospectedTableIbatis2Java5Impl.class.getName();
    } else if ("Ibatis3".equalsIgnoreCase(type)) { //$NON-NLS-1$
      type = IntrospectedTableMyBatis3Impl.class.getName();
    } else if ("MyBatis3".equalsIgnoreCase(type)) { //$NON-NLS-1$
      type = IntrospectedTableMyBatis3Impl.class.getName();
    } else if ("MyBatis3Simple".equalsIgnoreCase(type)) { //$NON-NLS-1$
      type = IntrospectedTableMyBatis3SimpleImpl.class.getName();
    }

    IntrospectedTable answer = (IntrospectedTable) createInternalObject(type);
    answer.setContext(context);

    return answer;
  }

  /**
   * Creates a new Object object.
   *
   * @param context
   *          the context
   * @return the introspected column
   */
  public static IntrospectedColumn createIntrospectedColumn(Context context) {
    String type = context.getIntrospectedColumnImpl();
    if (!stringHasValue(type)) {
      type = IntrospectedColumn.class.getName();
    }

    IntrospectedColumn answer = (IntrospectedColumn) createInternalObject(type);
    answer.setContext(context);

    return answer;
  }
}
